// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2024-07-31

package src

import (
	"context"
	"fmt"
	"gitee.com/go-libs/config"
	"gitee.com/go-libs/log"
	"sync"
	"time"
	"xorm.io/xorm"

	_ "github.com/go-sql-driver/mysql"
)

// Config
// is a singleton instance that constructed when package init.
var Config Configuration

type (
	// Configuration
	// is an interface for connections.
	Configuration interface {
		// Add
		// adds a database mapping to configuration.
		Add(key string, database *Database) (err error)

		// Del
		// deletes database from mapping and activated engine group.
		Del(key string)

		// Get
		// returns a shared database configuration.
		Get(keys ...string) (database *Database, exists bool)

		// GetEngine
		// returns a shared xorm engine group, creates a new when called
		// at first time.
		GetEngine(keys ...string) (*EngineGroup, error)

		// GetMaster
		// returns a shared xorm master session of a connection.
		GetMaster(ctx context.Context, keys ...string) (session *Session, err error)

		// GetSlave
		// returns a shared xorm slave session of a connection.
		GetSlave(ctx context.Context, keys ...string) (session *Session, err error)
	}

	configuration struct {
		// Databases
		// scanned from config file db.yml or db.yaml.
		Databases map[string]*Database `yaml:"databases"`

		engines map[string]*EngineGroup
		err     error
		mu      *sync.RWMutex
	}
)

// Add
// adds a database mapping to configuration.
func (o *configuration) Add(key string, database *Database) (err error) {
	// Return error
	// if key is blank string.
	if key == "" {
		err = fmt.Errorf(`database mapping key can not be blank`)
		return
	}

	// Return error
	// if connection is nil.
	if database == nil {
		err = fmt.Errorf(`database configuration can not be nil`)
		return
	}

	// Lock
	// in mutex and unlock when done.
	o.mu.Lock()
	defer o.mu.Unlock()

	// Return error
	// if database existed.
	if _, ok := o.Databases[key]; ok {
		err = fmt.Errorf(`database mapping key is existed: %s`, key)
		return
	}

	// Reset
	// database mapping.
	database.After()

	// Update
	// database mapping.
	o.Databases[key] = database
	return
}

// Del
// deletes database from mapping and activated engine group.
func (o *configuration) Del(key string) {
	// Do nothing
	// if mapping key is blank string.
	if key == "" {
		return
	}

	// Lock
	// in mutex and unlock when done.
	o.mu.Lock()
	defer o.mu.Unlock()

	// Update mapping.
	if _, ok := o.Databases[key]; ok {
		delete(o.Databases, key)
	}

	// Remove
	// used engine group.
	if v, ok := o.engines[key]; ok {
		delete(o.engines, key)

		// Close engine group and active sessions.
		if err := v.Close(); err != nil {
			log.Errorf(`close engine group failed: %s`, err)
		}
	}
}

// Get
// returns a shared database configuration.
func (o *configuration) Get(keys ...string) (database *Database, exists bool) {
	var key = DefaultKey

	// Use
	// custom key.
	if len(keys) > 0 && keys[0] != "" {
		key = keys[0]
	}

	// Lock
	// in mutex and unlock when done.
	o.mu.RLock()
	defer o.mu.RUnlock()

	// Read
	// from mapping with given key.
	database, exists = o.Databases[key]
	return
}

// GetEngine
// returns a shared xorm engine group.
func (o *configuration) GetEngine(keys ...string) (group *EngineGroup, err error) {
	var (
		cfg *Database
		key = DefaultKey
		ok  bool
	)

	// Return error
	// if initialize failed.
	if o.err != nil {
		err = o.err
		return
	}

	// Use
	// custom key.
	if len(keys) > 0 && keys[0] != "" {
		key = keys[0]
	}

	// Lock
	// in mutex and unlock when done.
	o.mu.Lock()
	defer o.mu.Unlock()

	// Reuse
	// latest engine.
	if group, ok = o.engines[key]; ok {
		return
	}

	// Return error
	// if specified database not configured or not added.
	if cfg, ok = o.Databases[key]; !ok {
		err = fmt.Errorf(`database not in mapping: %s`, key)
		return
	}

	// Create
	// engine group.
	if group, err = xorm.NewEngineGroup(cfg.Driver, cfg.Dsn); err != nil {
		return
	}

	// Build
	// initial mapper type on database, table and field name.
	group.SetMapper(cfg.mapper)

	// Build
	// connection pool attributes.
	group.SetMaxIdleConns(cfg.MaxIdle)
	group.SetMaxOpenConns(cfg.MaxOpen)
	group.SetConnMaxLifetime(time.Duration(cfg.MaxLifetime) * time.Second)

	// Build
	// additional attributes.
	group.EnableSessionID(*cfg.EnableSession)
	group.ShowSQL(*cfg.EnableLogger)

	// Build
	// logger adapter.
	group.SetLogger(&logger{cfg: cfg, key: key})

	// Update
	// engine mapping.
	o.engines[key] = group
	return
}

// GetMaster
// returns a shared xorm master session of a connection.
func (o *configuration) GetMaster(ctx context.Context, keys ...string) (session *Session, err error) {
	var group *EngineGroup
	if group, err = o.GetEngine(keys...); err == nil {
		session = group.Master().Context(ctx)
	}
	return
}

// GetSlave
// returns a shared xorm slave session of a connection.
func (o *configuration) GetSlave(ctx context.Context, keys ...string) (session *Session, err error) {
	var group *EngineGroup
	if group, err = o.GetEngine(keys...); err == nil {
		session = group.Slave().Context(ctx)
	}
	return
}

// +---------------------------------------------------------------------------+
// | Hook methods                                                              |
// +---------------------------------------------------------------------------+

// After
// this method is called by config hook. Do not call it directly.
func (o *configuration) After() {
	// Initialize.
	if o.Databases == nil {
		o.Databases = make(map[string]*Database)
	}

	// Call
	// database after method to set default values.
	for _, x := range o.Databases {
		x.After()
	}
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *configuration) init() *configuration {
	o.err = config.Seek("db.yml", "db.yaml").ScanYaml(o)
	o.engines = make(map[string]*EngineGroup)
	o.mu = &sync.RWMutex{}
	return o
}
